chore: use gt.config.json and _gt/<locale>.json for example translations#20
chore: use gt.config.json and _gt/<locale>.json for example translations#20moss-bryophyta wants to merge 3 commits intogeneraltranslation:mainfrom
Conversation
Refactor all examples to: - Read configuration from gt.config.json - Store translations in _gt/<locale>.json files - Use a load_translations function that reads from _gt/ directory The custom load_translations function is still required for now but will be removed in a future release.
| print(f"[eager] Loading translations for '{locale}'") | ||
| return TRANSLATIONS.get(locale, {}) | ||
| """Load translations from _gt/<locale>.json.""" | ||
| path = GT_DIR / f"{locale}.json" |
There was a problem hiding this comment.
Unsanitized locale in file path
The locale string is taken directly from the Accept-Language header (after determine_locale resolves it) and used to construct a file path without any validation. A crafted header containing traversal sequences such as ../../secrets would build a path like _gt/../../secrets.json, potentially reaching files outside the _gt/ directory.
While the appended .json extension and the path.exists() guard reduce practical risk, a defence-in-depth approach would validate that the resolved locale only contains safe characters before using it in path construction:
def load_translations(locale: str) -> dict[str, str]:
"""Load translations from _gt/<locale>.json."""
import re
if not re.fullmatch(r"[a-zA-Z0-9_-]+", locale):
return {}
path = GT_DIR / f"{locale}.json"
if path.exists():
with open(path) as f:
return json.load(f)
return {}The same pattern appears in all four examples:
examples/fastapi-lazy/app.py:25examples/flask-eager/app.py:25examples/flask-lazy/app.py:25
Prompt To Fix With AI
This is a comment left during a code review.
Path: examples/fastapi-eager/app.py
Line: 25
Comment:
**Unsanitized locale in file path**
The `locale` string is taken directly from the `Accept-Language` header (after `determine_locale` resolves it) and used to construct a file path without any validation. A crafted header containing traversal sequences such as `../../secrets` would build a path like `_gt/../../secrets.json`, potentially reaching files outside the `_gt/` directory.
While the appended `.json` extension and the `path.exists()` guard reduce practical risk, a defence-in-depth approach would validate that the resolved locale only contains safe characters before using it in path construction:
```python
def load_translations(locale: str) -> dict[str, str]:
"""Load translations from _gt/<locale>.json."""
import re
if not re.fullmatch(r"[a-zA-Z0-9_-]+", locale):
return {}
path = GT_DIR / f"{locale}.json"
if path.exists():
with open(path) as f:
return json.load(f)
return {}
```
The same pattern appears in all four examples:
- `examples/fastapi-lazy/app.py:25`
- `examples/flask-eager/app.py:25`
- `examples/flask-lazy/app.py:25`
How can I resolve this? If you propose a fix, please make it concise.| with open(BASE_DIR / "gt.config.json") as f: | ||
| config = json.load(f) |
There was a problem hiding this comment.
Bare open() at module level with no error handling
gt.config.json is opened at module level without any error handling. If the file is missing (e.g., the app is started from a different working directory, or the file is accidentally excluded from a deployment), Python will raise an unhandled FileNotFoundError that immediately aborts the import with an opaque traceback — no indication of which file is missing or how to fix it.
Notably, initialize_gt already accepts a config_path parameter and delegates to load_gt_config(), which includes proper error handling (returns an empty config when the file is absent, raises a descriptive ValueError for malformed JSON). Since the apps manually pass the resolved values anyway, the manual load is redundant; using config_path directly would be cleaner:
initialize_gt(
app,
config_path=str(BASE_DIR / "gt.config.json"),
load_translations=load_translations,
eager_loading=True,
)If the manual load is intentional (e.g., to access the config values in other parts of the module), at minimum wrap it in a try/except with a descriptive message:
try:
with open(BASE_DIR / "gt.config.json") as f:
config = json.load(f)
except FileNotFoundError as exc:
raise FileNotFoundError(
f"gt.config.json not found at {BASE_DIR / 'gt.config.json'}. "
"Please create it or run the app from the correct directory."
) from excThe same pattern appears in all four examples:
examples/fastapi-lazy/app.py:19-20examples/flask-eager/app.py:19-20examples/flask-lazy/app.py:20-21
Prompt To Fix With AI
This is a comment left during a code review.
Path: examples/fastapi-eager/app.py
Line: 19-20
Comment:
**Bare `open()` at module level with no error handling**
`gt.config.json` is opened at module level without any error handling. If the file is missing (e.g., the app is started from a different working directory, or the file is accidentally excluded from a deployment), Python will raise an unhandled `FileNotFoundError` that immediately aborts the import with an opaque traceback — no indication of which file is missing or how to fix it.
Notably, `initialize_gt` already accepts a `config_path` parameter and delegates to `load_gt_config()`, which includes proper error handling (returns an empty config when the file is absent, raises a descriptive `ValueError` for malformed JSON). Since the apps manually pass the resolved values anyway, the manual load is redundant; using `config_path` directly would be cleaner:
```python
initialize_gt(
app,
config_path=str(BASE_DIR / "gt.config.json"),
load_translations=load_translations,
eager_loading=True,
)
```
If the manual load is intentional (e.g., to access the config values in other parts of the module), at minimum wrap it in a `try/except` with a descriptive message:
```python
try:
with open(BASE_DIR / "gt.config.json") as f:
config = json.load(f)
except FileNotFoundError as exc:
raise FileNotFoundError(
f"gt.config.json not found at {BASE_DIR / 'gt.config.json'}. "
"Please create it or run the app from the correct directory."
) from exc
```
The same pattern appears in all four examples:
- `examples/fastapi-lazy/app.py:19-20`
- `examples/flask-eager/app.py:19-20`
- `examples/flask-lazy/app.py:20-21`
How can I resolve this? If you propose a fix, please make it concise.
Refactors all four examples (fastapi-eager, fastapi-lazy, flask-eager, flask-lazy) to:
gt.config.json—defaultLocaleandlocalesare now defined in a config file rather than hardcoded in the app_gt/<locale>.json— each locale gets its own JSON file with hash-keyed translationsload_translationsfunction that reads from the_gt/directoryThe custom
load_translationsfunction is still required for now but will be removed in a future release.Tests are unchanged — they create their own self-contained app fixtures.
Greptile Summary
This PR refactors all four example applications (fastapi-eager, fastapi-lazy, flask-eager, flask-lazy) to externalize configuration and translations: locale settings move from hardcoded values to
gt.config.json, and translation strings move from inline dictionaries to per-locale_gt/<locale>.jsonfiles. The changes align the examples with the intended file-based workflow that users of the library would follow in practice.Key changes across all four apps:
defaultLocaleandlocalesare now read fromgt.config.jsonvia a module-leveljson.load()call.TRANSLATIONSdict is replaced by aload_translations(locale)function that reads_gt/<locale>.jsonfrom disk, returning{}for unknown locales.print()statements removed: The[eager]/[lazy]loading prints that were in the oldload_translationsfunctions are gone — a clean improvement."en"omitted fromlocalesin config: Thegt.config.jsonfiles only list["es", "fr"]. This is correct becauseI18nManageralways addsdefault_localeto its internal locale set, so"en"is still supported at runtime.Confidence Score: 4/5
app.pyfiles all share the same two patterns worth fixing before this code is promoted as a copy-paste reference for users.Important Files Changed
Sequence Diagram
sequenceDiagram participant App as app.py (module load) participant CFG as gt.config.json participant GT as initialize_gt() participant MGR as I18nManager participant LDR as load_translations(locale) participant FS as _gt/<locale>.json App->>CFG: open & json.load() CFG-->>App: {defaultLocale, locales} App->>GT: initialize_gt(default_locale, locales, load_translations, eager_loading) GT->>MGR: I18nManager(default_locale, locales=[en,es,fr]) Note over MGR: Internally adds default_locale to locales set alt eager_loading=True MGR->>LDR: load_translations("en") LDR->>FS: open _gt/en.json (not found) LDR-->>MGR: {} MGR->>LDR: load_translations("es") LDR->>FS: open _gt/es.json FS-->>LDR: {hash: "Hola, mundo!", ...} LDR-->>MGR: translations dict MGR->>LDR: load_translations("fr") LDR->>FS: open _gt/fr.json FS-->>LDR: {hash: "Bonjour, le monde!", ...} LDR-->>MGR: translations dict end Note over App,MGR: App ready — translations cached in managerLast reviewed commit: 09d2f1f